/*
 *
 *  Copyright (C) 2010 Tal Eisenberg
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */


package fork.modbox;

import fork.Modbox;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.StringTokenizer;
import javax.swing.JPanel;
import javax.swing.JViewport;
import javax.swing.Scrollable;
import tal.drivechain.chain.Chain;
import tal.drivechain.chain.ChainNode;
import tal.drivechain.chain.ChainNode.MSG;

/**
 *
 * @author eisental
 */
public class NodeGraph extends JPanel implements Scrollable, MouseListener, MouseMotionListener {
    Modbox owner;
    private Color color,  darker,  xor;
    private HashMap<ChainNode, NodeCube> cubes = new HashMap<ChainNode, NodeCube>();
    private boolean tempLine = false;
    private Point tempP1;
    private Point tempP2;
    private Point oP, dP;
    private Dimension preferredScrollableSize = new Dimension(500,300);
    private NewPopup newPopup;
    private SingleNodePopup singleNodePopup;
    private MultiNodePopup multiNodePopup;
    private List<NodeCube> selectedCubes = new ArrayList<NodeCube>();
    public NodeGraph(Modbox owner, Color color, Font font) {
        this.setLayout(null);
        this.owner = owner;
        this.addMouseListener(this);
        this.addMouseMotionListener(this);
        newPopup = new NewPopup(owner);
        singleNodePopup = new SingleNodePopup(owner);
        multiNodePopup = new MultiNodePopup(owner);
        setColor(color);
        setFont(font);
        setPreferredSize(new Dimension(640,480));
    }

    @Override
    public void setSize(Dimension d) {
        Dimension oldSize = getSize();
        super.setSize(d);

        if ((oldSize.height*oldSize.width)==0)
            for (NodeCube c : cubes.values())
                c.addNotify();
    }

    public void addNodeCube(ChainNode node) {
        NodeCube m = new NodeCube(node, owner, color, this.getFont());
        m.setFont(getFont());
        cubes.put(node, m);
        this.add(m);
    }

    public Collection<NodeCube> getCubes() { return cubes.values(); }

    public Color getColor() {
        return color;
    }

    public void setColor(Color color) {
        this.color = color;
        this.darker = color.darker();
        this.xor = new Color(color.getRed() ^ 255, color.getGreen() ^ 255, color.getBlue() ^ 255);
        for (NodeCube c : cubes.values())
            c.setColor(color);
        repaint();
        if (newPopup!=null) this.newPopup.setColor(color);
        if (singleNodePopup!=null) this.singleNodePopup.setColor(color);
        if (multiNodePopup!=null) this.multiNodePopup.setColor(color);
    }

    @Override
    public void setFont(Font font) {
        super.setFont(font);
        if (cubes != null) {
            for (NodeCube m : cubes.values()) {
                if (m != null) {
                    m.setFont(font);
                }
            }
        }
        if (newPopup!=null) this.newPopup.setFont(font);
        if (singleNodePopup!=null) this.singleNodePopup.setFont(font);
        if (multiNodePopup!=null) this.multiNodePopup.setFont(font);
    }

    public SingleNodePopup getSingleNodePopup() { return singleNodePopup; }

    public MultiNodePopup getMultiNodePopup() { return multiNodePopup; }

    public void readModLocations(String locs) {
        // instance : x,y/

        StringTokenizer t = new StringTokenizer(locs, "/");
        while (t.hasMoreElements()) {
            String modloc = t.nextToken();
            int colonIdx = modloc.indexOf(':');
            if (colonIdx == -1) {
                owner.log("Invalid modbox meta syntax: ..." + modloc + "/...", MSG.WARNING);
            } else {
                String instance = modloc.substring(0, colonIdx).trim();
                String xy = modloc.substring(colonIdx + 1);
                int comIdx = xy.indexOf(',');
                if (comIdx == -1) {
                    owner.log("Invalid modbox meta syntax: ..." + modloc + "/...", MSG.WARNING);
                } else {
                    try {
                        int x = Integer.parseInt(xy.substring(0, comIdx));
                        int y = Integer.parseInt(xy.substring(comIdx + 1));
                        ChainNode node = null;
                        for (ChainNode n : cubes.keySet()) {
                            if (n.getName().equals(instance)) {
                                node = n;
                                break;
                            }
                        }
                        if (node == null) {
                            owner.log("Unknown module instance in modbox meta: ..." + modloc + "/...", MSG.WARNING);
                        } else {
                            NodeCube cube = cubes.get(node);
                            cube.setLocation(x, y);
                            cube.setHasLocation(true);
                        }
                    } catch (NumberFormatException e) {
                        owner.log("Invalid modbox meta syntax: ..." + modloc + ";...", MSG.WARNING);
                    }
                }
            }
        }
        owner.updateChainMeta();
        updateSize();
    }

    public void removeNodeCube(ChainNode node) {
        NodeCube c = cubes.get(node);
        if (c==null)
            throw new IllegalArgumentException("Module " + node.getName() + " is not part of chain " + ((Chain)owner.getActualBoxChain()).getPath());
        else {
            node.removeNodeListener(c);
            cubes.remove(node);
            this.remove(c);
            owner.updateChainMeta();
            owner.getGUI().getChildrenPanel().refresh();
        }
    }

    public void clear() {
        removeAll();
        cubes.clear();
        selectedCubes.clear();
        repaint();
    }

    public Point center(int width, int height) {
        return new Point((getWidth() - width) / 2, (getHeight() - height) / 2);
    }

    public String makeLocationString() {
        // <instance>:x,y/.../
        String ret = "";
        for (NodeCube cube : cubes.values()) {
            ret += cube.getChainNode().getName() + ":" + cube.getX() + "," + cube.getY() + "/";
        }
        if (ret.length()==0) return "";
        else return ret.substring(0, ret.length() - 1);
    }

    public void updateSize() {
        int maxX = 0, maxY = 0;
        for (NodeCube cube : cubes.values()) {
            int x = cube.getX() + cube.getWidth();
            if (x > maxX) {
                maxX = x;
            }

            int y = cube.getY() + cube.getHeight();
            if (y > maxY) {
                maxY = y;
            }
        }
        if (maxX < 640) {
            maxX = 640;
        }
        if (maxY < 480) {
            maxY = 480;
        }

        if (getPreferredSize().width!=maxX+50 || getPreferredSize().height!=maxY+50) {
            this.setPreferredSize(new Dimension(maxX+50, maxY+50));
            JViewport parent = (JViewport)getParent();
            int viewX = maxX - parent.getSize().width;
            int viewY = maxY - parent.getSize().height;
            parent.scrollRectToVisible(new Rectangle(viewX, viewY, maxX+50-viewX, maxY+50-viewY));
        }

        repaint();
        owner.getGUI().revalidate();

        this.revalidate();
    }

    public void setTempLine(Point p1, Point p2) {
        tempLine = true;
        tempP1 = p1;
        tempP2 = p2;
    }

    public void removeTempLine() {
        tempLine = false;
        tempP1 = null;
        tempP2 = null;
        repaint();
    }

    @Override
    protected void paintComponent(Graphics g) {
        //super.paintComponent(g);
        g.setColor(Color.BLACK);
        JViewport parent = (JViewport) getParent();
        int x = parent.getViewPosition().x;
        int y = parent.getViewPosition().y;
        int width = parent.getExtentSize().width + x;
        int height = parent.getExtentSize().height + y;
        g.setClip(x, y, width, height);
        g.fillRect(0, 0, width, height);
        for (ChainNode node : cubes.keySet()) {
            if (!node.getNodeClassName().equals("OutTerminal")) {
                for (ChainNode output : node.getOutputs()) {
                    paintModLine(cubes.get(node), cubes.get(output), g);
                }
            }
        }
        if (tempLine) paintLine(tempP1.x, tempP1.y, tempP2.x, tempP2.y, true, g);

        super.paintComponents(g);
        if (oP!=null) {
            paintSelectionBox(g);
            g.drawRect(oP.x, oP.y, dP.x - oP.x, dP.y - oP.y);
        }
    }

    private void paintSelectionBox(Graphics g) {
        g.setColor(xor);
        g.drawLine(oP.x, oP.y, dP.x, oP.y); // corner -> right
        g.drawLine(dP.x, oP.y, dP.x, dP.y); // right -> down
        g.drawLine(dP.x, dP.y, oP.x, dP.y); // down -> left
        g.drawLine(oP.x, dP.y, oP.x, oP.y); // left -> up
    }

    private void paintModLine(NodeCube src, NodeCube dest, Graphics g) {
        boolean selected = (src.isSelected() || dest.isSelected());
        Rectangle s = src.getBounds();
        Rectangle d = dest.getBounds();
        int x0 = s.x + s.width;
        int y0 = s.y + s.height / 2;
        int x1 = d.x;
        int y1 = d.y + d.height / 2;
        if (x1-x0>6)
            paintStraightModLine(x0,y0,x1,y1,selected,g);
        else {
            if (x1>x0 && ((y1<y0+s.height+3 && y1>y0) || (y0<y1+d.height+3 && y0>y1)))
                // in case the 2 node cubes are too close together we paint the normal line.
                paintLine(x0,y0,x1,y1,selected,g);
            else {
                int outY, inY;
                if (y0>y1) { // go up
                    outY = y0-(s.height/2)-2;
                inY = y1+(d.height/2)+2;
                } else { // go down
                    outY = y0+(s.height/2)+2;
                    inY = y1-(d.height/2)-2;
                }

                // create the output curved line
                paintLine(x0,y0,x0+4,y0,selected,g); 
                paintLine(x0+4,y0, x0+4, outY,selected,g);

                // create the input curved line
                paintLine(x1, y1, x1-4, y1, selected, g);
                paintLine(x1-4,y1, x1-4, inY, selected, g);

                // connect both curved lines
                paintLine(x0+4, outY, x1-4, inY, selected, g);
            }
        }
    }

    private void paintStraightModLine(int x0, int y0, int x1, int y1, boolean selected, Graphics g) {
        // paint straight output edge
        paintLine(x0,y0,x0+4,y0,selected,g);

        // paint straight input edge
        paintLine(x1,y1,x1-4,y1, selected, g);

        // connect them
        paintLine(x0+4,y0,x1-4,y1, selected, g);
    }

    private void paintLine(int x0, int y0, int x1, int y1, boolean selected, Graphics g) {
        Graphics2D g2d = (Graphics2D)g;
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        if (selected) {
            g2d.setColor(color.brighter());
            g2d.setStroke(new BasicStroke(2, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_ROUND));
        } else {
            g.setColor(xor.brighter());
            g2d.setStroke(new BasicStroke(1));
        }

        g2d.drawLine(x0, y0, x1, y1);
    }

    public void mouseClicked(MouseEvent e) {
        if (e.getButton()==MouseEvent.BUTTON1) {
            if (e.getClickCount()==2) {

            } else {
                for (NodeCube cube : cubes.values())
                    cube.setSelected(false);
                owner.sendSelectionChange();
            }
        } else if (e.getButton()==MouseEvent.BUTTON3) {
            // show the new node frame at e.getPoint()
            newPopup.setLocation(e.getLocationOnScreen());
            newPopup.setVisible(true);
        }
    }

    public void mousePressed(MouseEvent e) {
        oP = e.getPoint();
        dP = oP;
        repaint();
    }

    public void mouseReleased(MouseEvent e) {
        if (dP!=null && oP!=null) {
            int lowX = Math.min(dP.x, oP.x);
            int highX = Math.max(dP.x, oP.x);
            int lowY = Math.min(dP.y, oP.y);
            int highY = Math.max(dP.y, oP.y);
            for (NodeCube c : cubes.values()) {
                if (c.getLocation().x>lowX && c.getLocation().x<highX && c.getLocation().y>lowY && c.getLocation().y<highY)
                    c.setSelected(true);
            }

            owner.sendSelectionChange();

        }
        oP = null;
        dP = null;
        repaint();
    }

    public void mouseEntered(MouseEvent e) {

    }

    public void mouseExited(MouseEvent e) {
    }

    public void mouseDragged(MouseEvent e) {
        dP = e.getPoint();
        repaint();
    }

    public void mouseMoved(MouseEvent e) {
    }

    public Dimension getPreferredScrollableViewportSize() {
        return preferredScrollableSize;
    }

    public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
        return 5;
    }

    public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) {
        return 20;
    }

    public boolean getScrollableTracksViewportWidth() {
        return false;
    }

    public boolean getScrollableTracksViewportHeight() {
        return false;
    }

    void clearSelection() {
        for (NodeCube mcube : owner.getGUI().getGraph().getCubes())
            mcube.setSelected(false);
        selectedCubes.clear();

    }

    public List<NodeCube> getSelectedCubes() {
        return this.selectedCubes;
    }

}
